Skip to content

Comments

Implement customizable task dispatch interceptor scheme#112

Open
conradbzura wants to merge 3 commits intomainfrom
task-interceptor
Open

Implement customizable task dispatch interceptor scheme#112
conradbzura wants to merge 3 commits intomainfrom
task-interceptor

Conversation

@conradbzura
Copy link
Contributor

@conradbzura conradbzura commented Feb 21, 2026

Summary

Implement a customizable interceptor system for task dispatch that allows users to hook into the task lifecycle — modifying tasks before dispatch and wrapping response streams during execution. Interceptors follow an async generator protocol with three phases: pre-dispatch, stream processing, and cleanup.

Add an InterceptorLike protocol, an @interceptor decorator for global registration, and a WoolInterceptor bridge that adapts Wool interceptors to gRPC's server interceptor interface. Integrate interceptor support into LocalWorker and WorkerProcess. Export new public API symbols from wool/__init__.py.

Closes #105

Proposed changes

1. Interceptor protocol and registration

Add wool/src/wool/runtime/routine/interceptor.py with:

  • InterceptorLike — a Protocol defining the async generator contract: yield a modified Task (or None) pre-dispatch, receive the response stream, and yield wrapped events.
  • @interceptor — decorator that appends to a global registry.
  • get_registered_interceptors() — return a copy of the registry.

2. gRPC bridge

Add WoolInterceptor(AsyncServerInterceptor) in the same module. The bridge deserializes the task from the protobuf request, runs each interceptor's pre-dispatch phase in forward order, re-serializes if modified, calls the real dispatch method, then wraps the response stream through each interceptor in reverse order. Only apply to dispatch RPC calls — other methods (e.g., stop) bypass interception.

3. Worker integration

  • LocalWorker accepts an optional interceptors parameter; defaults to globally registered interceptors. Pass an empty list to disable.
  • WorkerProcess receives the interceptor list and creates a WoolInterceptor when starting the gRPC server.

4. Public API and housekeeping

  • Export get_registered_interceptors and interceptor from wool/__init__.py.
  • Add grpc-interceptor to pyproject.toml dependencies.

Test cases

Test Suite Test ID Given When Then Coverage Target
TestInterceptorDecorator IN-001 An interceptor function @interceptor applied Function added to global registry interceptor()
TestInterceptorDecorator IN-002 An interceptor function @interceptor applied Original function returned unchanged interceptor()
TestInterceptorDecorator IN-003 Multiple interceptor functions Each decorated with @interceptor All registered in order interceptor()
TestInterceptorDecorator IN-004 Same function decorated twice @interceptor applied twice Function appears twice in registry interceptor()
TestGetRegisteredInterceptors IN-005 No registered interceptors get_registered_interceptors() called Empty list returned get_registered_interceptors()
TestGetRegisteredInterceptors IN-006 One registered interceptor get_registered_interceptors() called List with the interceptor returned get_registered_interceptors()
TestGetRegisteredInterceptors IN-007 Multiple registered interceptors get_registered_interceptors() called All returned in registration order get_registered_interceptors()
TestGetRegisteredInterceptors IN-008 Registered interceptors exist Returned list mutated Original registry unaffected get_registered_interceptors()
TestWoolInterceptor IN-009 A list of interceptors WoolInterceptor instantiated Interceptors stored __init__()
TestWoolInterceptor IN-010 An empty interceptor list WoolInterceptor instantiated Bridge created successfully __init__()
TestWoolInterceptor IN-011 Bridge with no interceptors intercept() called for dispatch Method called directly intercept() early exit
TestWoolInterceptor IN-012 Bridge with interceptors intercept() called for non-dispatch method Method called without interception intercept() bypass
TestWoolInterceptor IN-013 Passthrough interceptor yielding None intercept() called for dispatch Task dispatched unmodified intercept() passthrough
TestWoolInterceptor IN-014 Interceptor yielding modified task intercept() called for dispatch Modified task serialized and dispatched intercept() task modification
TestWoolInterceptor IN-015 Multiple passthrough interceptors intercept() called for dispatch Pre-dispatch forward order, stream reverse order intercept() ordering
TestWoolInterceptor IN-016 Multiple task-modifying interceptors intercept() called for dispatch Modifications chain through each interceptor intercept() chained modification
TestWoolInterceptor IN-017 Stream-wrapping interceptor intercept() called for dispatch Events returned from wrapped stream intercept() stream wrapping
TestWoolInterceptor IN-018 Stream-filtering interceptor intercept() called for dispatch Only matching events returned intercept() stream filtering
TestWoolInterceptor IN-019 Stream-injecting interceptor intercept() called for dispatch Original and injected events returned intercept() stream injection
TestWoolInterceptor IN-020 Multiple stream wrappers intercept() called for dispatch Wrappers applied in reverse order intercept() reverse wrapping
TestWoolInterceptor IN-021 Interceptor raising pre-dispatch intercept() called for dispatch Exception propagates to caller intercept() error handling
TestWoolInterceptor IN-022 Interceptor raising StopAsyncIteration pre-dispatch intercept() called for dispatch Treated as passthrough intercept() edge case
TestWoolInterceptor IN-023 Interceptor raising during stream wrapping intercept() called for dispatch Exception propagates to caller intercept() error handling
TestWoolInterceptor IN-024 Interceptor returning without yielding events intercept() called for dispatch Original stream used intercept() edge case
TestWoolInterceptor IN-025 Dispatch method raises exception intercept() called for dispatch Exception propagates to caller intercept() error handling
TestWoolInterceptor IN-026 Full lifecycle interceptor intercept() called for dispatch Task modified, stream wrapped, phases ordered intercept() full lifecycle
TestWoolInterceptor IN-027 0–5 passthrough interceptors (property) intercept() called for dispatch All original events returned unchanged intercept() idempotency
TestWoolInterceptor IN-028 0–100 events, 0–5 passthroughs (property) Events flow through chain Output count equals input count intercept() preservation
TestWoolInterceptor IN-029 1–10 tracking interceptors (property) intercept() called twice Ordering deterministic across runs intercept() determinism
TestWoolInterceptor IN-030 Any exception at any position/phase (property) intercept() called for dispatch Exception propagates to caller intercept() universality

Implementation plan

    • Add InterceptorLike protocol, @interceptor decorator, and get_registered_interceptors() in interceptor.py
    • Implement WoolInterceptor with pre-dispatch, stream-wrapping, and error-handling logic
    • Write comprehensive test suite for decorator, registry, and bridge (test_interceptor.py)
    • Integrate interceptors into LocalWorker and WorkerProcess
    • Export new public API symbols from wool/__init__.py
    • Add grpc-interceptor dependency to pyproject.toml

Required by the new task dispatch interceptor scheme to bridge Wool
interceptors to the gRPC async server interceptor interface.
@wool-labs wool-labs bot added the code-change Indicates that a PR should trigger a release label Feb 21, 2026
Introduce a two-phase interceptor system for task and stream
manipulation at the gRPC layer. Interceptors enable extensible
processing pipelines for cross-cutting concerns like logging,
authentication, and metrics without modifying core worker logic.

InterceptorLike protocol defines the async generator contract:
pre-dispatch task modification followed by response stream wrapping.
The @interceptor decorator provides automatic global registration.
WoolInterceptor bridges the Wool interceptor interface to gRPC's
AsyncServerInterceptor.

LocalWorker and WorkerProcess accept an optional interceptors list,
falling back to globally registered interceptors when not specified.
Cover InterceptorLike protocol compliance, @interceptor decorator
registration, WoolInterceptor gRPC bridge (dispatch-only filtering,
task modification, stream wrapping, multi-interceptor chaining, error
propagation), and updated public API surface assertions.
@conradbzura conradbzura added the feature New feature or capability label Feb 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

code-change Indicates that a PR should trigger a release feature New feature or capability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement customizable task dispatch interceptor scheme

1 participant